Tutorial: 03 - GESTIÓN DE ESTADOS CON REDUX

03 - GESTIÓN DE ESTADOS CON REDUX



1. Qué es Redux y Redux Toolkit

Redux es una biblioteca para la gestión de los estados en las interfaces de usuario que conforman los componentes de una aplicación React. Redux Toolkit ha simplificado todavía más el flujo de trabajo sobre la gestión de estados que ya hacía Redux.

Las herramientas que Redux y Redux Toolkit ponen a disposición del desarrollador frontend serían las siguientes:

Componentes useState y useEffect

Redux trae varios componentes en su librería basados en useState y useEffect que nos podemos importar a nuestra aplicación. Redux también tiene acceso a ala API Context de React.

Store

Redux permite crear el store, que podemos entender como una base de datos de la aplicación front, en la que se guarda un estado global al que todos los componentes pueden acceder para saber si se tienen que actualizar, y repintarse en la vista del navegador web.

Slice

El store que creamos con Redux, se puede (y se debe) subdividir en "porciones" que llamamos slices. El directorio de Redux estará compuesto por el store, y los slices en los que se divide, todos ficheros de JavaScript

Reducers y acciones

Redux permite dar de alta en el store (y en sus slices) una serie de métodos llamados reducers que sirven para modificar el estado global del store cuando se ejecuta una acción determinada. Cuando damos de alta un reducer, Redux crea estas acciones que se recibirán junto con un payload como parámetros para ser enviados al store, y actualizar la interfaz de usuario. Cuando nos referimos convencionalmente a este proceso de llamada al store decimos que estamos "despachando una acción".



2. Esquema conceptual flujo





3. Empezar proyecto e instalación

Vamos a explicar el funcionamiento general con un pequeño ejemplo práctico. Iniciamos un proyecto con Vite.

npm create vite nombre-proyecto

Instalamos Redux y Redux toolkit

npm i @reduxjs/toolkit react-redux



En la siguiente imagen se muestra la estructura de ficheros de nuestro ejemplo, ahora analizaremos los ficheros relevantes.





4. Crear Slice user

Vamos a empezar por el fichero userSlice.js.

Nos traemos el módulo createSlice de Redux Toolkit.

import { createSlice } from "@reduxjs/toolkit";

Creamos y exportamos una constante donde vamos a utilizar el método createSlice() que recibirá un objeto. Este objeto lo conformarán el nombre del slice, el estado inicial y un objeto con los métodos reducers. Viendo este caso práctico en particular, vemos que los reducers son addUser() y changeEmail().

El estado inicial, lo sacaremos en un objeto a parte, que luego le pasaremos a userSlice.

const initialState = {
    name: "",
    username: "",
    email: ""
};


export const userSlice = createSlice({
    name: "user",
    initialState,
    reducers:{
        addUser: (state, action) => {

        },
        changeEmail: (state, action) => {

        }
    },
})

5. Reducers y actions

Seguimos en el fichero userSlice.js.

Los reducers del slice, van a recibir un estado y una acción. Cuando los componentes utilicen el reducer, tendremos que despachar la acción. Esto significa que Redux, al escuchar a los reducers que atienden esas acciones gestionará la modificación de los estados de los componentes que estén llamando al store. Aquí es donde tenemos guardados los slices, con sus respectivos reducers.

Lo bueno, es que cuando damos de alta un reducer como estamos haciendo ahora, Redux creará automáticamente una acción del mismo nombre, lo cual hace que en la práctica este proceso sea menos complejo de lo que parece inicialmene.

Como decíamos, los reducers que hemos creado recibirán un estado y una acción. La acción contiene una propiedad payload que recibe como valor un objeto. Este objeto lo tenemos que dar de alta al crear el reducer, y lo hacemos con una sintaxis de extracción muy similar a la que vimos en el curso de iniciación de React. Esta sintaxis es la misma de destructuración de objetos en JavaScript ES6, y que utilizamos de manera habitual al traernos módulos y componentes.

A dar de alta este objeto, determinamos qué valores se tendrán que pasar como payload. En nuestro caso name, username, email.

El estado, también es un objeto que va a agrupar la propiedad, o las propiedades que hayamos definido en el payload.

Sabiendo esto, podemos escribir el código del reducer addUser.

    reducers:{
        addUser: (state, action) => {
            const {name, username, email} = action.payload; 
            state.name = name;
            state.username = username;
            state.email = email;
        },
        changeEmail: (state, action) => {

        }
    },

En el caso de changeEmail no es necesario utilizar la sintaxis de destructuración, ya que solo usamos un valor, y hay que crear un objeto.

    reducers:{
        addUser: (state, action) => {
            const {name, username, email} = action.payload; 
            state.name = name;
            state.username = username;
            state.email = email;
        },
        changeEmail: (state, action) => {
            state.email = action.payload;
        }
    },

Por último exportamos los reducers, y las acciones. Como ya decíamos, el mismo Redux crea una acción para cada Redux automáticamente, con lo cual, el slice ya contiene un objeto actions.

export const {addUser, changeEmail} = userSlice.actions;
export default  userSlice.reducer;

El fichero userSlice.js quedaría tal que así

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
    name: "",
    username: "",
    email: ""
};


export const userSlice = createSlice({
    name: "user",
    initialState,
    reducers:{
        addUser: (state, action) => {
            const {name, username, email} = action.payload; 
            state.name = name;
            state.username = username;
            state.email = email;
        },
        changeEmail: (state, action) => {
            state.email = action.payload;
        }
    },
})

export const {addUser, changeEmail} = userSlice.actions;
export default userSlice.reducer;
 



6. Crear Store

Pasamos al fichero store.js. Este fichero agrupará los diferentes slices que creemos Necesitamos el módulo configureStore de Redux Toolkit para dar de alta la store. Importamos también el userSlice.js que hemos creado también.

import { configureStore } from "@reduxjs/toolkit";
import useReducer  from "./userSlice";

A continuación configuramos el método configureStore. Aquí creamos un objeto con los reducers. En nuestro caso, solo tenemos el userSlice.js. Finalmente, lo exportamos el store. El fichero del store nos quedaría tal que así.


import { configureStore } from "@reduxjs/toolkit";
import useReducer  from "./userSlice";

export const store = configureStore({
    reducer: {
        user: useReducer
    }
});

export default store



7. Hacer accesible el store a la aplicación React

Ya tenemos el store creado, con su correspondiente slice. Vamos ahora al fichero main.jsx

Para traer compartir el store con toda la App, vamos a utilizar un componente de Redux que nos facilita un Provider. Recordemos que en el curso de iniciación a React vimos la creación del contexto, y como cada contexto podía dar de alta un Provider. Redux realiza esta gestión internamente, y pone a nuestra disposición este Provider.

Nos traemos el Provider de Redux y el store que hemos creado.

import { Provider } from 'react-redux'
import store from './redux/store'

En el HTML/JSX de la parte inferior, vamos a envolver el componente App en el componente Provider, y le pasaremos un atributo store. De esta manera todos los componentes tendrán acceso. El fichero nos quedará tal que así.

import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import store from './redux/store'
import App from './App.jsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
)



8. Llamada Fetch

Vamos ahora al fichero App.jsx

Lo primero, como siempre, las importaciones pertinentes en la parte superior. Hemos creado dos componentes de prueba Header.jsx y Email.jsx desde los que realizaremos las llamadas al store. Primero vamos a realizar una llamada Fetch desde el componente App para que comparta los datos con estos dos componentes.

import { Header } from "./components/Header";
import { Email } from "./components/Email";

A continuación, nos traemos dos hooks. useDispatch desde Redux Toolkit, y useEffect, que ya conocemos, desde el core de React.

import { useEffect } from "react";
import { addUser } from "./redux/userSlice";

Guardamos en una constante el método useDispatch() que hemos traido en el módulo.

const dispatch = useDispatch();

A continuación utilizamos useEffect() que nos permiten acceder a diferentes estados en el ciclos de vida del componente. En este caso, queremos que cuando el componente se cree, hagamos una llamada a un API, para que nos devuelva ciertos datos, los cuales nos llevaremos a la store. Utilizaremos jsonplaceholder.typicode.com y la API fetch del navegador para hacer la llamada.

Usaremos el método addUser que hemos creado en el slice y useDispatch para despachar la acción, y para que el store sepa que tiene que actualizar los estados.

El código del fichero nos quedaría tal que así.

import { Header } from "./components/Header";
import { Email } from "./components/Email";
import { useDispatch } from "react-redux";
import { useEffect } from "react";
import { addUser } from "./redux/userSlice";
import './App.css'

const App = () => {
  const dispatch = useDispatch();
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users/1")
      .then((response) => response.json())
      .then((data) => dispatch(addUser(data)))
      .catch((error)=> console.log(error)) 
  })
  return (
    <div className='App'>
      <Header />
      <Email />
    </div>
  );
}

export default App


9. Header.jsx: Acceso al store con useSelector

En este fichero, lo que vamos a hacer es sencillo, porque ya estamos haciendo la petición a la API para que nuestro store tenga datos. Lo único que tenemos que tener en cuenta, es que necesitamos importar un método de Redux llamado useSelector().

UseSelector nos permite acceder al store global, y a su vez, a los slices, en este caso, user.

import { useSelector } from "react-redux";

export const Header = () => {
    const user = useSelector((state) => state.user)
    return (
        <header>
            <h1>
                Ejemplo de Redux Toolkit
            </h1>
            <ul>
                <li>Nombre: {user.name}</li>
                <li>Email: {user.email}</li>
                <li>Nombre de usuario: {user.userename}</li>
            </ul>
        </header>
    )
}
10. Email.jsx: Actualizar el store mediante eventos

En el primer caso, simplemente hemos accedido a nuestro store, y le hemos pedido datos. Ahora vamos a disparar un evento onChange desde el frontend que despache la acción, de tal manera que el store se actualice y Redux repinte los componentes.

Como siempre, lo primero, las importaciones. Como en este caso tenemos que llamar y despachar vamos a usar useSelector y useDispatch. También cogemos el método changeEmail de nuestro slice, a través del destructuring.

import { useSelector, useDispatch } from "react-redux"
import { changeEmail } from "../redux/userSlice"

Si nos fijamos en el siguiente código, veremos que hacemos lo mismo que hacíamos en App y en Header. Solo que en este caso solo en un fichero. Vamos a retornar un input que lanzará el evento onChange.

export const Email = () => {
    const dispatch = useDispatch()
    const email = useSelector((state) => state.user.email)

    return (
        <input type="email" placeholder="email" onChange={} />
    )
}

Podríamos meter la función directamente en el onChange, pero vamos a meterla en una función handleChange() para una mejor función de los eventos. Accedemos al valor del evento a través del objeto event. Este trigger va a ser el que, al disparar el evento, despache la acción utilizando el método changeEmail, con el valor introducido en el input.

const handleChange = (e) => {
    dispatch(changeEmail(e.target.value))
}

Nuestro fichero quedaría tal que así.

import { useSelector, useDispatch } from "react-redux"
import { changeEmail } from "../redux/userSlice"

export const Email = () => {
    const dispatch = useDispatch()
    const email = useSelector((state) => state.user.email)

    const handleChange = (e) => {
        dispatch(changeEmail(e.target.value))
    }
    return (
        <input
            type="email"
            value={email}
            placeholder="email"
            onChange={handleChange}
        />
    )
}




Con esto, ya tendríamos una muy básica aplicación React, que gestiona los estados con Redux, pidiéndole datos a la store y escribiéndolos a través de un input en la interfaz de usuario. Vamos a dar un último tip.



10. Debug con Redux: Extensión de navegador

Redux, además tiene una consola de depuración que complementa la que ya instalamos de React para nuestro navegador: ReduxDevTools. Esta herramienta nos permite ver en vivo, y en diferentes formatos de visualización, el log que se ha ido haciendo de cambios de estado en la interfaz de usuario.





Cuenta con una barra en la parte inferior que permite avanzar atrás y adelante en el tiempo, y ver cómo los estados han ido cambiando, tanto en el log, como en la vizualización de la interfaz en el navegador de manera simultánea.

Otra característica muy útil, es la visualización en formato grafo de la aplicación, lo que permite ver de manera gráfica, como se mueven el estado, las acciones, etc.